/*
 * Circular Character Buffer
 * Copyright (C) 2002 Stephen Ostermiller
 * http://ostermiller.org/contact.pl?regarding=Java+Utilities
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * See COPYING.TXT for details.
 */
package org.opu.stylepane.Ostermiller.util;

import java.io.Reader;
import java.io.Writer;
import java.io.IOException;

/**
 * Implements the Circular Buffer producer/consumer model for characters.
 * More information about this class is available from <a target="_top" href=
 * "http://ostermiller.org/utils/CircularCharBuffer.html">ostermiller.org</a>.
 * <p>
 * Using this class is a simpler alternative to using a PipedReader
 * and a PipedWriter. PipedReaders and PipedWriters don't support the
 * mark operation, don't allow you to control buffer sizes that they use,
 * and have a more complicated API that requires instantiating two
 * classes and connecting them.
 * <p>
 * This class is thread safe.
 *
 * @see CircularByteBuffer
 * @see CircularObjectBuffer
 *
 * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities
 * @since ostermillerutils 1.00.00
 */
public class CircularCharBuffer {

    /**
     * The default size for a circular character buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    private final static int DEFAULT_SIZE = 1024;
    /**
     * A buffer that will grow as things are added.
     *
     * @since ostermillerutils 1.00.00
     */
    public final static int INFINITE_SIZE = -1;
    /**
     * The circular buffer.
     * <p>
     * The actual capacity of the buffer is one less than the actual length
     * of the buffer so that an empty and a full buffer can be
     * distinguished.  An empty buffer will have the markPostion and the
     * writePosition equal to each other.  A full buffer will have
     * the writePosition one less than the markPostion.
     * <p>
     * There are three important indexes into the buffer:
     * The readPosition, the writePosition, and the markPosition.
     * If the Reader has never been marked, the readPosition and
     * the markPosition should always be the same.  The characters
     * available to be read go from the readPosition to the writePosition,
     * wrapping around the end of the buffer.  The space available for writing
     * goes from the write position to one less than the markPosition,
     * wrapping around the end of the buffer.  The characters that have
     * been saved to support a reset() of the Reader go from markPosition
     * to readPosition, wrapping around the end of the buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected char[] buffer;
    /**
     * Index of the first character available to be read.
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile int readPosition = 0;
    /**
     * Index of the first character available to be written.
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile int writePosition = 0;
    /**
     * Index of the first saved character. (To support stream marking.)
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile int markPosition = 0;
    /**
     * Number of characters that have to be saved
     * to support mark() and reset() on the Reader.
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile int markSize = 0;
    /**
     * If this buffer is infinite (should resize itself when full)
     *
     * @since ostermillerutils 1.00.00
     */
    protected volatile boolean infinite = false;
    /**
     * True if a write to a full buffer should block until the buffer
     * has room, false if the write method should throw an IOException
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean blockingWrite = true;
    /**
     * The Reader that can empty this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected Reader reader = new CircularCharBufferReader();
    /**
     * true if the close() method has been called on the Reader
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean readerClosed = false;
    /**
     * The Writer that can fill this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected Writer writer = new CircularCharBufferWriter();
    /**
     * true if the close() method has been called on the writer
     *
     * @since ostermillerutils 1.00.00
     */
    protected boolean writerClosed = false;

    /**
     * Make this buffer ready for reuse.  The contents of the buffer
     * will be cleared and the streams associated with this buffer
     * will be reopened if they had been closed.
     *
     * @since ostermillerutils 1.00.00
     */
    public void clear() {
        synchronized (this) {
            readPosition = 0;
            writePosition = 0;
            markPosition = 0;
            readerClosed = false;
            writerClosed = false;
        }
    }

    /**
     * Retrieve a Writer that can be used to fill
     * this buffer.
     * <p>
     * Write methods may throw a BufferOverflowException if
     * the buffer is not large enough.  A large enough buffer
     * size must be chosen so that this does not happen or
     * the caller must be prepared to catch the exception and
     * try again once part of the buffer has been consumed.
     *
     *
     * @return the producer for this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    public Writer getWriter() {
        return writer;
    }

    /**
     * Retrieve a Reader that can be used to empty
     * this buffer.
     * <p>
     * This Reader supports marks at the expense
     * of the buffer size.
     *
     * @return the consumer for this buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    public Reader getReader() {
        return reader;
    }

    /**
     * Get number of characters that are available to be read.
     * <p>
     * Note that the number of characters available plus
     * the number of characters free may not add up to the
     * capacity of this buffer, as the buffer may reserve some
     * space for other purposes.
     *
     * @return the size in characters of this buffer
     *
     * @since ostermillerutils 1.00.00
     */
    public int getAvailable() {
        synchronized (this) {
            return available();
        }
    }

    /**
     * Get the number of characters this buffer has free for
     * writing.
     * <p>
     * Note that the number of characters available plus
     * the number of characters free may not add up to the
     * capacity of this buffer, as the buffer may reserve some
     * space for other purposes.
     *
     * @return the available space in characters of this buffer
     *
     * @since ostermillerutils 1.00.00
     */
    public int getSpaceLeft() {
        synchronized (this) {
            return spaceLeft();
        }
    }

    /**
     * Get the capacity of this buffer.
     * <p>
     * Note that the number of characters available plus
     * the number of characters free may not add up to the
     * capacity of this buffer, as the buffer may reserve some
     * space for other purposes.
     *
     * @return the size in characters of this buffer
     *
     * @since ostermillerutils 1.00.00
     */
    public int getSize() {
        synchronized (this) {
            return buffer.length;
        }
    }

    /**
     * double the size of the buffer
     *
     * @since ostermillerutils 1.00.00
     */
    private void resize() {
        char[] newBuffer = new char[buffer.length * 2];
        int marked = marked();
        int available = available();
        if (markPosition <= writePosition) {
            // any space between the mark and
            // the first write needs to be saved.
            // In this case it is all in one piece.
            int length = writePosition - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length);
        } else {
            int length1 = buffer.length - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length1);
            int length2 = writePosition;
            System.arraycopy(buffer, 0, newBuffer, length1, length2);
        }
        buffer = newBuffer;
        markPosition = 0;
        readPosition = marked;
        writePosition = marked + available;
    }

    /**
     * Space available in the buffer which can be written.
     *
     * @since ostermillerutils 1.00.00
     */
    private int spaceLeft() {
        if (writePosition < markPosition) {
            // any space between the first write and
            // the mark except one character is available.
            // In this case it is all in one piece.
            return (markPosition - writePosition - 1);
        } else {
            // space at the beginning and end.
            return ((buffer.length - 1) - (writePosition - markPosition));
        }
    }

    /**
     * Characters available for reading.
     *
     * @since ostermillerutils 1.00.00
     */
    private int available() {
        if (readPosition <= writePosition) {
            // any space between the first read and
            // the first write is available.  In this case i
            // is all in one piece.
            return (writePosition - readPosition);
        } else {
            // space at the beginning and end.
            return (buffer.length - (readPosition - writePosition));
        }
    }

    /**
     * Characters saved for supporting marks.
     *
     * @since ostermillerutils 1.00.00
     */
    private int marked() {
        if (markPosition <= readPosition) {
            // any space between the markPosition and
            // the first write is marked.  In this case i
            // is all in one piece.
            return (readPosition - markPosition);
        } else {
            // space at the beginning and end.
            return (buffer.length - (markPosition - readPosition));
        }
    }

    /**
     * If we have passed the markSize reset the
     * mark so that the space can be used.
     *
     * @since ostermillerutils 1.00.00
     */
    private void ensureMark() {
        if (marked() >= markSize) {
            markPosition = readPosition;
            markSize = 0;
        }
    }

    /**
     * Create a new buffer with a default capacity.
     * Writing to a full buffer will block until space
     * is available rather than throw an exception.
     *
     * @since ostermillerutils 1.00.00
     */
    public CircularCharBuffer() {
        this(DEFAULT_SIZE, true);
    }

    /**
     * Create a new buffer with given capacity.
     * Writing to a full buffer will block until space
     * is available rather than throw an exception.
     * <p>
     * Note that the buffer may reserve some characters for
     * special purposes and capacity number of characters may
     * not be able to be written to the buffer.
     * <p>
     * Note that if the buffer is of INFINITE_SIZE it will
     * neither block or throw exceptions, but rather grow
     * without bound.
     *
     * @param size desired capacity of the buffer in characters or CircularCharBuffer.INFINITE_SIZE
     *
     * @since ostermillerutils 1.00.00
     */
    public CircularCharBuffer(int size) {
        this(size, true);
    }

    /**
     * Create a new buffer with a default capacity and
     * given blocking behavior.
     *
     * @param blockingWrite true writing to a full buffer should block
     *        until space is available, false if an exception should
     *        be thrown instead.
     *
     * @since ostermillerutils 1.00.00
     */
    public CircularCharBuffer(boolean blockingWrite) {
        this(DEFAULT_SIZE, blockingWrite);
    }

    /**
     * Create a new buffer with the given capacity and
     * blocking behavior.
     * <p>
     * Note that the buffer may reserve some characters for
     * special purposes and capacity number of characters may
     * not be able to be written to the buffer.
     * <p>
     * Note that if the buffer is of CircularCharBuffer.INFINITE_SIZE it will
     * neither block or throw exceptions, but rather grow
     * without bound.
     *
     * @param size desired capacity of the buffer in characters or CircularCharBuffer.INFINITE_SIZE
     * @param blockingWrite true writing to a full buffer should block
     *        until space is available, false if an exception should
     *        be thrown instead.
     *
     * @since ostermillerutils 1.00.00
     */
    public CircularCharBuffer(int size, boolean blockingWrite) {
        if (size == INFINITE_SIZE) {
            buffer = new char[DEFAULT_SIZE];
            infinite = true;
        } else {
            buffer = new char[size];
            infinite = false;
        }
        this.blockingWrite = blockingWrite;
    }

    /**
     * Class for reading from a circular character buffer.
     *
     * @since ostermillerutils 1.00.00
     */
    protected class CircularCharBufferReader extends Reader {

        /**
         * Close the stream. Once a stream has been closed, further read(), ready(),
         * mark(), or reset() invocations will throw an IOException. Closing a
         * previously-closed stream, however, has no effect.
         *
         * @throws IOException never.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void close() throws IOException {
            synchronized (CircularCharBuffer.this) {
                readerClosed = true;
            }
        }

        /**
         * Mark the present position in the stream. Subsequent calls to reset() will
         * attempt to reposition the stream to this point.
         * <p>
         * The readAheadLimit must be less than the size of circular buffer.
         *
         * @param readAheadLimit Limit on the number of characters that may be read while
         *    still preserving the mark. After reading this many characters, attempting to
         *    reset the stream will fail.
         * @throws IOException if the stream is closed, or the buffer size is greater
         * than or equal to the readAheadLimit.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void mark(int readAheadLimit) throws IOException {
            synchronized (CircularCharBuffer.this) {
                if (readerClosed) {
                    throw new IOException("Reader has been closed; cannot mark a closed Reader.");
                }
                if (buffer.length - 1 <= readAheadLimit) {
                    throw new IOException("Cannot mark stream, readAheadLimit bigger than buffer size.");
                }
                markSize = readAheadLimit;
                markPosition = readPosition;
            }
        }

        /**
         * Tell whether this stream supports the mark() operation.
         *
         * @return true, mark is supported.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public boolean markSupported() {
            return true;
        }

        /**
         * Read a single character.
         * This method will block until a character is available, an I/O error occurs,
         * or the end of the stream is reached.
         *
         * @return The character read, as an integer in the range 0 to 65535 (0x00-0xffff),
         *     or -1 if the end of the stream has been reached
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public int read() throws IOException {
            while (true) {
                synchronized (CircularCharBuffer.this) {
                    if (readerClosed) {
                        throw new IOException("Reader has been closed; cannot read from a closed Reader.");
                    }
                    int available = available();
                    if (available > 0) {
                        int result = buffer[readPosition] & 0xffff;
                        readPosition++;
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return result;
                    } else if (writerClosed) {
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (Exception x) {
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }

        /**
         * Read characters into an array.
         * This method will block until some input is available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param cbuf Destination buffer.
         * @return The number of characters read, or -1 if the end of
         *   the stream has been reached
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public int read(char[] cbuf) throws IOException {
            return read(cbuf, 0, cbuf.length);
        }

        /**
         * Read characters into a portion of an array.
         * This method will block until some input is available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param cbuf Destination buffer.
         * @param off Offset at which to start storing characters.
         * @param len Maximum number of characters to read.
         * @return The number of characters read, or -1 if the end of
         *   the stream has been reached
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public int read(char[] cbuf, int off, int len) throws IOException {
            while (true) {
                synchronized (CircularCharBuffer.this) {
                    if (readerClosed) {
                        throw new IOException("Reader has been closed; cannot read from a closed Reader.");
                    }
                    int available = available();
                    if (available > 0) {
                        int length = Math.min(len, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        System.arraycopy(buffer, readPosition, cbuf, off, firstLen);
                        if (secondLen > 0) {
                            System.arraycopy(buffer, 0, cbuf, off + firstLen, secondLen);
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (writerClosed) {
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (Exception x) {
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }

        /**
         * Tell whether this stream is ready to be read.
         *
         * @return True if the next read() is guaranteed not to block for input,
         *    false otherwise. Note that returning false does not guarantee that
         *    the next read will block.
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public boolean ready() throws IOException {
            synchronized (CircularCharBuffer.this) {
                if (readerClosed) {
                    throw new IOException("Reader has been closed, it is not ready.");
                }
                return (available() > 0);
            }
        }

        /**
         * Reset the stream.
         * If the stream has been marked, then attempt to reposition i
         * at the mark. If the stream has not been marked, or more characters
         * than the readAheadLimit have been read, this method has no effect.
         *
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void reset() throws IOException {
            synchronized (CircularCharBuffer.this) {
                if (readerClosed) {
                    throw new IOException("Reader has been closed; cannot reset a closed Reader.");
                }
                readPosition = markPosition;
            }
        }

        /**
         * Skip characters.
         * This method will block until some characters are available,
         * an I/O error occurs, or the end of the stream is reached.
         *
         * @param n The number of characters to skip
         * @return The number of characters actually skipped
         * @throws IllegalArgumentException if n is negative.
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public long skip(long n) throws IOException, IllegalArgumentException {
            while (true) {
                synchronized (CircularCharBuffer.this) {
                    if (readerClosed) {
                        throw new IOException("Reader has been closed; cannot skip characters on a closed Reader.");
                    }
                    int available = available();
                    if (available > 0) {
                        int length = Math.min((int) n, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        if (secondLen > 0) {
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (writerClosed) {
                        return 0;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (Exception x) {
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }
    }

    /**
     * Class for writing to a circular character buffer.
     * If the buffer is full, the writes will either block
     * until there is some space available or throw an IOException
     * based on the CircularCharBuffer's preference.
     *
     * @since ostermillerutils 1.00.00
     */
    protected class CircularCharBufferWriter extends Writer {

        /**
         * Close the stream, flushing it first.
         * This will cause the reader associated with this circular buffer
         * to read its last characters once it empties the buffer.
         * Once a stream has been closed, further write() or flush() invocations
         * will cause an IOException to be thrown. Closing a previously-closed stream,
         * however, has no effect.
         *
         * @throws IOException never.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void close() throws IOException {
            synchronized (CircularCharBuffer.this) {
                if (!writerClosed) {
                    flush();
                }
                writerClosed = true;
            }
        }

        /**
         * Flush the stream.
         *
         * @throws IOException if the stream is closed.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void flush() throws IOException {
            if (writerClosed) {
                throw new IOException("Writer has been closed; cannot flush a closed Writer.");
            }
            if (readerClosed) {
                throw new IOException("Buffer closed by Reader; cannot flush.");
            }
        // this method needs to do nothing
        }

        /**
         * Write an array of characters.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param cbuf Array of characters to be written
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void write(char[] cbuf) throws IOException {
            write(cbuf, 0, cbuf.length);
        }

        /**
         * Write a portion of an array of characters.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param cbuf Array of characters
         * @param off Offset from which to start writing characters
         * @param len - Number of characters to write
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            while (len > 0) {
                synchronized (CircularCharBuffer.this) {
                    if (writerClosed) {
                        throw new IOException(
                                "Writer has been closed; cannot write to a closed Writer.");
                    }
                    if (readerClosed) {
                        throw new IOException("Buffer closed by Reader; cannot write to a closed buffer.");
                    }
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < len) {
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < len) {
                        throw new BufferOverflowException(
                                "CircularCharBuffer is full; cannot write "
                                + len + " characters");
                    }
                    int realLen = Math.min(len, spaceLeft);
                    int firstLen = Math.min(realLen, buffer.length - writePosition);
                    int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1);
                    int written = firstLen + secondLen;
                    if (firstLen > 0) {
                        System.arraycopy(cbuf, off, buffer, writePosition, firstLen);
                    }
                    if (secondLen > 0) {
                        System.arraycopy(cbuf, off + firstLen, buffer, 0, secondLen);
                        writePosition = secondLen;
                    } else {
                        writePosition += written;
                    }
                    if (writePosition == buffer.length) {
                        writePosition = 0;
                    }
                    off += written;
                    len -= written;
                }
                if (len > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception x) {
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }

        /**
         * Write a single character.
         * The character to be written is contained in the 16 low-order bits of the
         * given integer value; the 16 high-order bits are ignored.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param c int specifying a character to be written.
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void write(int c) throws IOException {
            boolean written = false;
            while (!written) {
                synchronized (CircularCharBuffer.this) {
                    if (writerClosed) {
                        throw new IOException(
                                "Writer has been closed; cannot write to a closed Writer.");
                    }
                    if (readerClosed) {
                        throw new IOException(
                                "Buffer closed by Reader; cannot write to a closed buffer.");
                    }
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < 1) {
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < 1) {
                        throw new BufferOverflowException(
                                "CircularCharBuffer is full; cannot write 1 character");
                    }
                    if (spaceLeft > 0) {
                        buffer[writePosition] = (char) (c & 0xffff);
                        writePosition++;
                        if (writePosition == buffer.length) {
                            writePosition = 0;
                        }
                        written = true;
                    }
                }
                if (!written) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception x) {
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }

        /**
         * Write a string.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param str String to be written
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void write(String str) throws IOException {
            write(str, 0, str.length());
        }

        /**
         * Write a portion of a string.
         * If the buffer allows blocking writes, this method will block until
         * all the data has been written rather than throw an IOException.
         *
         * @param str A String
         * @param off Offset from which to start writing characters
         * @param len Number of characters to write
         * @throws BufferOverflowException if buffer does not allow blocking writes
         *   and the buffer is full.  If the exception is thrown, no data
         *   will have been written since the buffer was set to be non-blocking.
         * @throws IOException if the stream is closed, or the write is interrupted.
         *
         * @since ostermillerutils 1.00.00
         */
        @Override
        public void write(String str, int off, int len) throws IOException {
            while (len > 0) {
                synchronized (CircularCharBuffer.this) {
                    if (writerClosed) {
                        throw new IOException(
                                "Writer has been closed; cannot write to a closed Writer.");
                    }
                    if (readerClosed) {
                        throw new IOException(
                                "Buffer closed by Reader; cannot write to a closed buffer.");
                    }
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < len) {
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < len) {
                        throw new BufferOverflowException(
                                "CircularCharBuffer is full; cannot write "
                                + len + " characters");
                    }
                    int realLen = Math.min(len, spaceLeft);
                    int firstLen = Math.min(realLen, buffer.length - writePosition);
                    int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1);
                    int written = firstLen + secondLen;
                    for (int i = 0; i < firstLen; i++) {
                        buffer[writePosition + i] = str.charAt(off + i);
                    }
                    if (secondLen > 0) {
                        for (int i = 0; i < secondLen; i++) {
                            buffer[i] = str.charAt(off + firstLen + i);
                        }
                        writePosition = secondLen;
                    } else {
                        writePosition += written;
                    }
                    if (writePosition == buffer.length) {
                        writePosition = 0;
                    }
                    off += written;
                    len -= written;
                }
                if (len > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception x) {
                        throw new IOException(
                                "Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }
    }
}
